/*
 * Copyright 2017 hv0912
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *  http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.vimhe.ezimp2.handler.reader;

import com.vimhe.ezimp2.config.ImportConfig;
import com.vimhe.ezimp2.config.bean.ColumnConfig;
import com.vimhe.ezimp2.config.bean.FileConfig;
import com.vimhe.ezimp2.config.bean.RowConfig;
import com.vimhe.ezimp2.config.bean.SheetConfig;
import com.vimhe.ezimp2.exception.ImportException;
import com.vimhe.ezimp2.handler.Reader;
import com.vimhe.ezimp2.handler.bean.CellData;
import com.vimhe.ezimp2.handler.bean.RowData;
import com.vimhe.ezimp2.handler.bean.SheetData;
import com.vimhe.ezimp2.util.ExcelColumnIndexUtils;
import org.apache.poi.openxml4j.exceptions.OpenXML4JException;
import org.apache.poi.openxml4j.opc.OPCPackage;
import org.apache.poi.xssf.eventusermodel.ReadOnlySharedStringsTable;
import org.apache.poi.xssf.eventusermodel.XSSFReader;
import org.apache.poi.xssf.eventusermodel.XSSFSheetXMLHandler;
import org.apache.poi.xssf.usermodel.XSSFComment;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.XMLReader;
import org.xml.sax.helpers.XMLReaderFactory;

import java.io.IOException;
import java.io.InputStream;

/**
 * Excel2007 表格解析处理驱动
 *
 * @author Vimhe
 */
public final class Excel2007Handler {

    /**
     * 数据读取器
     */
    private Reader reader;

    /**
     * 配置管理对象
     */
    private ImportConfig importConfig;

    /**
     * 文件索引
     */
    private String fileIndex;

    /**
     * 当前解析到的工作表索引
     */
    private int sheetIndex;

    /**
     * 用于存储当前解析到的工作表的数据
     */
    private SheetData sheetData;

    /**
     * 构造器
     *
     * @param reader       数据读取器
     * @param importConfig 配置管理对象
     */
    public Excel2007Handler(Reader reader, ImportConfig importConfig) {
        this.reader = reader;
        this.importConfig = importConfig;
    }

    /**
     * 遍历 Excel 中的工作表
     * <br/>为了性能考虑，只会读取配置中指定的工作表
     *
     * @param fileIndex   文件索引
     * @param inputStream 文件流，使用后或出现异常时会自动关闭
     */
    public void process(String fileIndex, InputStream inputStream) {
        this.fileIndex = fileIndex;
        sheetIndex = 1;

        XSSFReader xssfReader;
        XMLReader xmlReader;
        try (InputStream autoClose = inputStream;
             OPCPackage opcPackage = OPCPackage.open(autoClose)) {
            xssfReader = new XSSFReader(opcPackage);

            xmlReader = XMLReaderFactory.createXMLReader();
            xmlReader.setContentHandler(new XSSFSheetXMLHandler(
                    xssfReader.getStylesTable(),
                    new ReadOnlySharedStringsTable(opcPackage),
                    new MyExcel2007SheetHandler(),
                    false));
        } catch (IOException | SAXException | OpenXML4JException e) {
            throw new ImportException("无法初始化Excel解析器", e);
        }

        // 只从配置中指定的工作表读取
        FileConfig fileConfig = importConfig.getFileConfig(fileIndex);
        if (fileConfig != null) {
            for (SheetConfig sheetConfig : fileConfig.getSheetConfigMap().values()) {
                sheetIndex = sheetConfig.getIndex();
                sheetData = new SheetData(fileIndex, sheetIndex);

                try (InputStream sheetSource = xssfReader.getSheet("rId" + sheetIndex)) {
                    xmlReader.parse(new InputSource(sheetSource));
                } catch (OutOfConfigRowEndException e) {
                    // 找到了指定行的数据就跳过当前工作表以提高效率
                } catch (Exception e) {
                    throw new ImportException("解析Excel过程中出现错误", e);
                }

                reader.read(sheetData, sheetConfig);
            }
        }

    }

    /**
     * 该异常是为了跳出当前正在处理的工作表
     * <br/>有时候我们可能只需要获取工作表中指定行的数据（范围访问），通常情况下解析器会解析整个工作表
     * <br/>通过该异常来实现手动跳出当前工作表的解析以提高范围访问效率
     */
    private class OutOfConfigRowEndException extends RuntimeException {
    }

    /**
     * 自定义 Excel 工作表解析处理驱动
     */
    private final class MyExcel2007SheetHandler implements XSSFSheetXMLHandler.SheetContentsHandler {

        /**
         * 空值
         */
        private static final String NULL_VALUE = "";

        /**
         * 当前解析到的行索引
         */
        private int rowIndex;

        /**
         * 当前解析到的单元格索引
         */
        private int cellIndex;

        /**
         * 上一次解析到的列索引
         */
        private int lastCellIndex;

        /**
         * 用于存储当前解析到的行的数据
         */
        private RowData rowData;

        @Override
        public void startRow(int rowNum) {
            rowIndex = rowNum + 1;
            cellIndex = 1;
            lastCellIndex = 0;
            rowData = new RowData(fileIndex, sheetIndex, rowIndex);
        }

        @Override
        public void endRow(int rowNum) {
            RowConfig rowConfig = importConfig.getRowConfig(fileIndex, sheetIndex, rowIndex);
            if (rowConfig != null) {
                int cellIndexCount = rowConfig.getColumnConfigMap().size();

                /*
                 * 整行为空则忽略，如果经过一行之后lastCellIndex的值仍然不变，证明该行是空行
                 * 处理行尾的空单元格，从上一次读取的单元格之后开始补齐，到指定的列索引为止
                 * 在同一行内，若后一次比前一次解析到的列索引相差大于1，证明中间没有解析到值
                 * 按照.xlsx底层是xml描述文件原理，此时对应xml中“空值”的情况
                 */
                for (int i = lastCellIndex + 1; i <= cellIndexCount; i++) {
                    cell(ExcelColumnIndexUtils.number2Index(i) + rowIndex, null, null);
                }

                reader.read(rowData, rowConfig);

                sheetData.addValue(rowIndex, rowData);

                // 找到了指定行的数据就跳过当前工作表以提高效率
                if (rowConfig.getEnd() <= rowIndex) {
                    throw new OutOfConfigRowEndException();
                }
            }
        }

        @Override
        public void cell(String cellReference, String formattedValue, XSSFComment comment) {
            // 转换数字（“A1”的列索引为“A”）
            String cellIndexString = cellReference.replaceAll("\\d+", NULL_VALUE);

            cellIndex = ExcelColumnIndexUtils.index2Number(cellIndexString);

            /*
             * 在同一行内，若后一次比前一次解析到的列索引相差大于1，证明中间没有解析到值
             * 按照.xlsx底层是xml描述文件原理，此时对应xml中“空值”的情况
             */
            if (cellIndex - lastCellIndex > 1) {
                for (int i = lastCellIndex + 1; i < cellIndex; i++) {
                    processCellData(fileIndex, sheetIndex, rowIndex, ExcelColumnIndexUtils.number2Index(i), NULL_VALUE);
                }
            }

            processCellData(fileIndex, sheetIndex, rowIndex, cellIndexString,
                    formattedValue == null ? NULL_VALUE : formattedValue);

            // 更新最后一次列索引计数
            lastCellIndex = ExcelColumnIndexUtils.index2Number(cellIndexString);
        }

        @Override
        public void headerFooter(String text, boolean isHeader, String tagName) {

        }

        /**
         * 处理单元格
         *
         * @param fileIndex  文件索引
         * @param sheetIndex 工作表索引
         * @param rowIndex   行索引
         * @param cellIndex  列索引
         * @param value      数据
         */
        private void processCellData(String fileIndex, Integer sheetIndex, Integer rowIndex, String cellIndex, String value) {
            // 构建单元格对象
            ColumnConfig columnConfig = importConfig.getColumnConfig(fileIndex, sheetIndex, rowIndex, cellIndex);
            if (columnConfig != null && columnConfig.getReadable()) {
                CellData cellData = new CellData(fileIndex, sheetIndex, rowIndex, cellIndex);
                cellData.setValue(value);

                reader.read(cellData, columnConfig);

                // 收集每个单元格的数据
                rowData.addValue(cellIndex, cellData);
            }
        }

    }

}
